﻿# -*- coding: utf-8 -*-
# iTunesIrcGateway for Crigate


import clr
import re
import Misuzilla.Crigate

from System                          import *
from System.Collections.Generic      import *
from System.Diagnostics              import Trace, Process
from System.Runtime.InteropServices  import Marshal
from Misuzilla.Crigate               import ConsoleHandler, Console, Context
from Misuzilla.Crigate.Configuration import IConfiguration, ICustomConfiguration, ConfigurationPropertyInfo
from Misuzilla.Crigate.Scripting     import DLRIntegrationHandler, DLRBasicConfiguration, DLRContextHelper
from Misuzilla.Net.Irc import *

class iTunesCollectionFilter(object):
	numregex = re.compile(r'^\d*-\d*$')

	def __init__(self, text=None):
		if text:
			self.parse(text)
		else:
			self.set()

	def set(self, name=None, start=None, end=None):
		self.name = name
		self.start = start
		self.end = end

	def parse(self, text):
		name = start = end = None
		if self.numregex.match(text):
			spec = text.split('-')
			start = spec.pop(0)
			if start:
				start = int(start, 10)
			else:
				start = None
			if spec:
				end = spec.pop(0)
				if end:
					end = int(end, 10)
				else:
					end = None
			else:
				# 今はここにくることはないけど
				end = start
		elif text.startswith('/') and text.endswith('/'):
			name = text[1:-1]
		else:
			name = text
		self.set(name, start, end)

	def __call__(self, collection, max=None):
		start = self.start
		if start is None or start < 1:
			start = 1
		end = self.end
		if end is None or end > collection.Count:
			end = collection.Count
		if start > end:
			start = end
		if self.name:
			namecond = re.compile(self.name).search
		else:
			namecond = None
		self.count = 0
		self.exceeded = False
		for i in range(start, end + 1):
			item = collection.Item(i)
			if namecond is None or namecond(item.Name):
				if max and self.count > max:
					self.exceeded = True
					return
				yield i, item
				self.count += 1


class iTunesConsole(object):
	def __init__(self, channel_name, session):
		self.channel_name = channel_name
		self.session = session
	
	def setup(self):
		# Unload
		CurrentSession.HandlerLoader.GetHandler[DLRIntegrationHandler]().BeforeUnload += self.onBeforeUnload
		
		# Console
		self.console = Misuzilla.Crigate.Console()
		self.console.Initialize(self.session)
		self.console.Attach(self.channel_name, DLRContextHelper.Wrap(CurrentSession, "iTunesContext", iTunesContext))

	def cleanup(self):
		# Console
		self.console.Detach()

	#region Events
	def onBeforeUnload(self, sender, args):
		self.cleanup()
	#endregion


def mark_as_command(func):
	func.context_command = True
	return func

class iTunesContext(Context):
	#region Context
	def Initialize(self, force=False):
		if force or Process.GetProcessesByName("iTunes"):
			self.connect("iTunes.Application")
			self.itunes = self.com_object
			if self.itunes.CurrentTrack:
				self.CurrentSession.SendServer(TopicMessage(self.Console.ConsoleChannelName, self.format_current_track()))
		else:
##			self.send_notice("iTunes が起動されていないので接続しません。")
			self.itunes = None
			self.com_object = None

	def GetCommands(self):
		d = Dictionary[String, String](Context.GetCommands(self))
		for k in sorted(dir(self.__class__)):
			try:
				v = getattr(self, k)
			except AttributeError:
				continue
			if getattr(v, 'context_command', False):
				d[k] = v.__doc__
		return d

	def OnUninitialize(self):
		self.disconnect()
	#endregion

	#region COM Connection
	def connect(self, progId):
		t = Type.GetTypeFromProgID(progId)
		self.com_object = Activator.CreateInstance(t)
		self.onCOMObjectConnected()

	def disconnect(self):
		self.onCOMObjectDisconnected()
		Marshal.FinalReleaseComObject(self.com_object)
		self.com_object = None

	@mark_as_command
	def reconnect(self, args):
		"""iTunes へ再接続する"""
		connecting = False
		force = args != ""
		try:
			junk = self.com_object.Version
			connecting = True
		except:
			pass
		if connecting:
			if not force:
				self.send_notice("接続は有効です。強制再接続を行う場合は force をつけてください。")
				return
			else:
				self.send_notice("再接続します。")
		if self.com_object:
			self.disconnect()
		self.Initialize(force)
		if self.itunes:
			self.send_notice("iTunes に接続しました。")
		else:
			self.send_notice("iTunes へ接続できませんでした。")
	#endregion

	#region COM Initialize
	def onCOMObjectConnected(self):
		self.com_object.OnCOMCallsEnabledEvent           += self.onCOMCallsEnabledEvent
		self.com_object.OnPlayerPlayingTrackChangedEvent += self.onPlayerPlayingTrackChangedEvent
		self.com_object.OnPlayerPlayEvent                += self.onPlayerPlayEvent
		self.com_object.OnPlayerStopEvent                += self.onPlayerStopEvent
		self.com_object.OnAboutToPromptUserToQuitEvent   += self.onAboutToPromptUserToQuitEvent

	def onCOMObjectDisconnected(self):
		self.com_object.OnCOMCallsEnabledEvent           -= self.onCOMCallsEnabledEvent
		self.com_object.OnPlayerPlayingTrackChangedEvent -= self.onPlayerPlayingTrackChangedEvent
		self.com_object.OnPlayerPlayEvent                -= self.onPlayerPlayEvent
		self.com_object.OnPlayerStopEvent                -= self.onPlayerStopEvent
		self.com_object.OnAboutToPromptUserToQuitEvent   -= self.onAboutToPromptUserToQuitEvent
	#endregion

	#region Events
	def onCOMCallsEnabledEvent(self):
		pass

	def onAboutToPromptUserToQuitEvent(self):
		self.disconnect()
		GC.Collect()
		self.send_notice("iTunes が終了するので接続を解除しました。")
		self.send_notice("再起動後に reconnect で再接続してください。")

	def onPlayerPlayingTrackChangedEvent(self, track):
		Trace.WriteLine("iTunesConsole: onPlayerPlayingTrackChangedEvent: "+track.ToString())
		#self.send_notice(self.format(track))
		self.CurrentSession.SendServer(TopicMessage(self.Console.ConsoleChannelName, self.format_current_track(track)))

	def onPlayerPlayEvent(self, track):
		Trace.WriteLine("iTunesConsole: onPlayerPlayEvent: "+track.ToString())
		#self.send_notice(self.format(track))
		self.CurrentSession.SendServer(TopicMessage(self.Console.ConsoleChannelName, self.format_current_track(track)))

	def onPlayerStopEvent(self, track):
		Trace.WriteLine("iTunesConsole: onPlayerStopEvent: "+track.ToString())
		# 一時停止したときだけ送信する
		if self.itunes.PlayerState == 0:
			self.CurrentSession.SendServer(TopicMessage(self.Console.ConsoleChannelName, self.format_current_track(track)))
			self.send_notice(self.format(track))
	#endregion

	#region Helper Methods
	def send_notice(self, message):
		self.Console.NotifyMessage(message)

	def format(self, track, i=None):
		states = []
		if track.Enabled:
			states.append("[x]")
		else:
			states.append("[ ]")
		if i is None:
			i = track.Index
		return "%d: %s %s / %s ( %s )" % (i, " ".join(states), track.Name, track.Artist, track.Album)

	def format_current_track(self, track=None):
		text = ""
		if self.itunes.PlayerState == 0:
			text += "[一時停止] "
		if track is None:
			track = self.itunes.CurrentTrack
		if not track:
			return ''
		num = int((track.Rating + 10) / 20)
		if track.RatingKind:
			symbol = '☆'
		else:
			symbol = '★'
		return text + self.format(track) + ": " + (symbol*num).ljust(5, '・')
	#endregion

	#region iTunes Control
	@mark_as_command
	def repeat(self, opt = None):
		"""リピートの設定 (引数: [one|all|none])"""
		if self.itunes.CanSetSongRepeat(self.itunes.CurrentPlaylist):
			if opt == None:
				self.itunes.CurrentPlaylist.SongRepeat = ((self.itunes.CurrentPlaylist.SongRepeat+1) % 3)
			else:
				opt = opt.Trim()
				if opt == "none":
					self.itunes.CurrentPlaylist.SongRepeat = 0
				elif opt == "one":
					self.itunes.CurrentPlaylist.SongRepeat = 1
				elif opt == "all":
					self.itunes.CurrentPlaylist.SongRepeat = 2

			if self.itunes.CurrentPlaylist.SongRepeat == 0:
				repeat_type = "リピートなし"
			elif self.itunes.CurrentPlaylist.SongRepeat == 1:
				repeat_type = "リピート(一曲)"
			elif self.itunes.CurrentPlaylist.SongRepeat == 2:
				repeat_type = "リピート(全曲)"

			self.send_notice(("リピートを %s に設定しました。" % repeat_type))
		else:
			self.send_notice("このプレイリストにリピートを設定することはできません")

	@mark_as_command
	def shuffle(self, opt = None):
		"""シャッフルの設定 (引数: [true|false])"""
		if self.itunes.CanSetSongRepeat(self.itunes.CurrentPlaylist):
			if opt == None:
				self.itunes.CurrentPlaylist.Shuffle = not self.itunes.CurrentPlaylist.Shuffle
			else:
				opt = opt.Trim()
				if opt == "true":
					self.itunes.CurrentPlaylist.Shuffle = True
				elif opt == "false":
					self.itunes.CurrentPlaylist.Shuffle = False
			self.send_notice(("シャッフルを %s に設定しました。" % ("有効" if self.itunes.CurrentPlaylist.Shuffle else "無効")))
		else:
			self.send_notice("このプレイリストにシャッフルを設定することはできません")

	@mark_as_command
	def playlists(self, opt=None):
		"""プレイリスト一覧 (引数: [start]-[end] もしくは [検索文字列])"""
		filter_ = iTunesCollectionFilter(opt)
		for i, playlist in filter_(self.itunes.LibrarySource.Playlists):
			self.send_notice(("%d: %s" % (i, playlist.Name)))
		if filter_.count:
			self.send_notice('%d playlist(s) found.' % filter_.count)
		else:
			self.send_notice('no playlists found.')

	@mark_as_command
	def select(self, opt = None):
		"""プレイリストを選択して再生を開始  (引数: index)"""
		self.itunes.LibrarySource.Playlists[int(opt, 10)].PlayFirstTrack()

	@mark_as_command
	def tracks(self, opt = None):
		"""現在のプレイリストの曲一覧 (引数: [start]-[end] もしくは [検索文字列])"""
		self.send_notice("プレイリスト: %s" % (self.itunes.CurrentPlaylist.Name))
		filter_ = iTunesCollectionFilter(opt)
		for i, track in filter_(self.itunes.CurrentPlaylist.Tracks):
			self.send_notice(self.format(track, i))
		if filter_.count:
			self.send_notice('%d track(s) found.' % filter_.count)
		else:
			self.send_notice('no tracks found.')

	@mark_as_command
	def check(self, args):
		"""チェックをつける (引数: index)"""
		self.toggleEnabled(True, args)
	@mark_as_command
	def uncheck(self, args):
		"""チェックをはずす (引数: index)"""
		self.toggleEnabled(False, args)
	def toggleEnabled(self, mode, opts):
		for num in re.split(r'[, \t]', opts):
			num = int(num, 10)
			self.itunes.CurrentPlaylist.Tracks.Item(num).Enabled = mode
			self.send_notice(self.format(self.itunes.CurrentPlaylist.Tracks.Item(num)))

	@mark_as_command
	def play(self, args):
		"""曲を再生します (引数: index)"""
		if args != "":
			self.itunes.CurrentPlaylist.Tracks.Item(int(args, 10)).Play()
		else:
			self.itunes.Play()
	@mark_as_command
	def next(self, args):
		"""次の曲へ"""
		self.itunes.NextTrack()
	@mark_as_command
	def prev(self, args):
		"""前の曲へ"""
		self.itunes.PreviousTrack()
	@mark_as_command
	def pause(self, args):
		"""一時停止"""
		self.itunes.Pause()
	@mark_as_command
	def Eval(self, args):
		"""任意の Python 文を実行します"""
		__builtins__['_'] = None
		it = itunes = self.itunes ## could you use these aliases in eval
		try:
			exec compile(args, "<input>", "single", 0)
		except BaseException, e:
			self.send_notice("error: " + repr(e))
		else:
			if _ is not None:
				if isinstance(_, basestring) and len(_.splitlines())==1:
					self.send_notice("%s: %s" % (_.__class__.__name__, _))
				else:
					self.send_notice(repr(_))
	@mark_as_command
	def current(self, args):
		"""現在の曲名を再表示"""
		if self.itunes.CurrentTrack:
			self.CurrentSession.SendServer(TopicMessage(self.Console.ConsoleChannelName, self.format_current_track()))
			def sec2string(sec):
				time = sec
				parts = []
				for div in [60, 60, 24]:
					time, part = divmod(time, div)
					parts.append(part)
					if not time:
						break
				else:
					part.append(time)
				while len(parts) < 2:
					parts.append(0)
				parts.reverse()
				ret = ""
				for part in parts:
					if ret:
						ret += ":%02d" % part
					else:
						ret = str(part)
				return ret
			pos = self.itunes.PlayerPosition
			duration = self.itunes.CurrentTrack.Duration
			self.send_notice("now: %s / %s" % (
				sec2string(pos), sec2string(duration)))
		else:
			self.send_notice("no track found")
	@mark_as_command
	def rate(self, args):
		"""レーティングを設定します (引数: rate)"""
		if args:
			num = int(args, 10)
			if num <= 5:
				# 0-5 を 0-100 に
				num *= 20
			self.itunes.CurrentTrack.Rating = num
		else:
			self.send_notice(self.format_current_track() + " (raw rate: %d)" % self.itunes.CurrentTrack.Rating)
	@mark_as_command
	def reveal(self, args):
		"""iTunes で表示します (引数: [(track|playlist) [index]])"""
		target = self.itunes.CurrentTrack
		if args:
			args = args.split(' ', 1)
			mode = args.pop(0).lower()
			if mode == 'playlist':
				if args:
					target = self.itunes.LibrarySource.Playlists[int(args.pop(0), 10)]
				else:
					target = self.itunes.CurrentPlaylist
			elif mode == 'track':
				if args:
					target = self.itunes.CurrentPlaylist.Tracks[int(args.pop(0), 10)]
				else:
					target = self.itunes.CurrentTrack
			else:
				self.send_notice('unknown argument: (track|playlist) [index]')
		if target:
			target.Reveal()

	@mark_as_command
	def update_ipod(self, args):
		"""iPod を同期します (引数: iPod name / 省略すると iPod の一覧を表示します)"""
		if not args:
			i = 0
			for source in self.itunes.Sources:
				if source.Kind != 2: continue
				i+=1
				self.send_notice('%d. %s' % (i, source.Name))
			self.send_notice('%d 個の iPod が接続されています。' % i)
			return
		source = self.itunes.Sources.ItemByName[args]
		if not source:
			# 完全一致しなかった。さらに別の方法で探す
			try:
				key = int(args)
			except ValueError:
				key = args.lower()
			for source in self.itunes.Sources:
				if source.Kind != 2: continue
				if isinstance(key, basestring):
					if source.Name.lower() == key:
						break
				else:
					key -= 1
					if not key:
						break
			else:
				source = None # reset
		if not source:
			self.send_notice('指定された iPod が見つかりません。')
			return
		if source.Kind != 2:
			self.send_notice('指定されたデバイスは iPod ではありません。')
			return
		self.send_notice('%s を同期します' % source.Name)
		source.UpdateIPod()
	#endregion

itunes_console = iTunesConsole("#iTunes", CurrentSession)
itunes_console.setup()
